﻿using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Globalization;
using System.Security.Permissions;
using Ns.Properties;

namespace Ns
{
	/// <summary>
    /// Represents a week defined by either a year and a week number or a date, calendar, calendar week rule and the first day of week.
	/// This class implements the comparison and addition/substraction operators.
	/// </summary>
    [Serializable]
    public struct Week : IComparable, IComparable<Week>, IEquatable<Week>, IFormattable, ISerializable
    {
        #region Members
        private readonly int value;
        private readonly DateTime begin;
        private readonly DateTime end;
        private readonly int year;
        private readonly int number;
        private readonly Calendar calendar;
        private readonly CalendarWeekRule calendarWeekRule;
        private readonly DayOfWeek firstDayOfWeek;
        #endregion

        #region Properties
        /// <summary>
        /// Gets the begin date of this week (included)
        /// </summary>
        /// <value>The begin date.</value>
        public DateTime Begin { get { return begin; } }

        /// <summary>
        /// Gets the end date of this week (included)
        /// </summary>
        /// <value>The end.</value>
        public DateTime End { get { return end; } }

        /// <summary>
        /// Gets the year of this week.
        /// </summary>
        public int Year { get { return year; } }

        /// <summary>
        /// Gets the week number.
        /// </summary>
        /// <value>The week number.</value>
        public int Number { get { return number; } }

        /// <summary>
        /// Gets the current iso week (today's week).
        /// </summary>
        /// <value>The current iso week.</value>
        public static Week CurrentIsoWeek
        {
            get { return NewIsoWeek(DateTime.Now); }
        }
        #endregion

        #region .Ctors
        public Week(int year, int number, Calendar calendar, CalendarWeekRule calendarWeekRule, DayOfWeek firstDayOfWeek)
        {
            this.calendar = calendar;
            this.calendarWeekRule = calendarWeekRule;
            this.firstDayOfWeek = firstDayOfWeek;

            this.year = year;
            this.number = number;

            var midYearDate = new DateTime(year, 6, 1);
            var midYearWeekNumber = calendar.GetWeekOfYear(midYearDate, calendarWeekRule, firstDayOfWeek);

            var date = calendar.AddWeeks(midYearDate, number - midYearWeekNumber);
            this.begin = GetFirstDayOfWeek(date, firstDayOfWeek);
            this.end = begin.AddDays(6);
            
            // Fix if W53 is also W1 of following year
            if (number == 53 && calendar.GetWeekOfYear(this.end, calendarWeekRule, firstDayOfWeek) == 1)
                this.value = end.Year * 100 + 1;
            else
                this.value = year * 100 + number;
        }

        public Week(DateTime date, Calendar calendar, CalendarWeekRule calendarWeekRule, DayOfWeek firstDayOfWeek)
        {
            this.calendar = calendar;
            this.calendarWeekRule = calendarWeekRule;
            this.firstDayOfWeek = firstDayOfWeek;

            this.begin = GetFirstDayOfWeek(date, firstDayOfWeek);
            this.end = begin.AddDays(6);

            this.number = calendar.GetWeekOfYear(date, calendarWeekRule, firstDayOfWeek);
            this.year = number < 52 ? date.Year : begin.Year;

            // Fix if W53 is also W1 of following year
            if (number == 53 && calendar.GetWeekOfYear(this.end, calendarWeekRule, firstDayOfWeek) == 1)
                this.value = end.Year * 100 + 1;
            else
                this.value = year * 100 + number;
        }

		private static DateTime GetFirstDayOfWeek(DateTime date, DayOfWeek firstDayOfWeek)
		{
			int difference = (int)date.DayOfWeek - (int)firstDayOfWeek;
			int delta = (difference < 0) ? difference += 7 : difference;

			return date.Date.AddDays(-delta);
		}
        #endregion

        #region Public methods
        public static Week NewIsoWeek(int year, int number)
        {
            return new Week(year, number, CultureInfo.InvariantCulture.Calendar, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
        }

        public static Week NewIsoWeek(DateTime date)
        {
            return new Week(date, CultureInfo.InvariantCulture.Calendar, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
        }        
        
        public static IEnumerable<Week> FromPeriod(Period period)
        {
            return FromPeriod(period, NewIsoWeek);
        }

        public static IEnumerable<Week> FromPeriod(Period period, Func<DateTime, Week> weekBuilder)
        {
            WeekPeriod wp = WeekPeriod.FromPeriod(period, weekBuilder);
            return wp.GetWeeks();
        }

        public Period ToPeriod()
        {
            //Add day because a period has by default end excluded and the end of this object is the sunday 00:00
            return new Period(Begin, End.AddDays(1));
        }

        public Week AddWeeks(int weeks)
        {
            var beginningOfNewWeek = calendar.AddWeeks(this.begin, weeks);
            return new Week(beginningOfNewWeek, calendar, calendarWeekRule, firstDayOfWeek);
        }

        /// <summary>
        /// Determines whether this week contains the specified date.
        /// </summary>
        /// <param name="date">The date.</param>
        /// <returns>
        /// 	<c>true</c> if this week contains the specified date; otherwise, <c>false</c>.
        /// </returns>
        public bool Contains(DateTime date)
        {
            return date >= Begin && date <= End;
        }

        /// <summary>
        /// Determines whether this week contains the specified date.
        /// </summary>
        /// <param name="date">The date.</param>
        /// <returns>
        /// 	<c>true</c> if the week contains the specified date; otherwise, <c>false</c>.
        /// </returns>
        public bool Contains(DateTimeOffset date)
        {
            return this.Contains(date.DateTime);
        }

        /// <summary>
        /// Determines whether this week is between two dates.
        /// </summary>
        /// <param name="startWeek">The startweek.</param>
        /// <param name="endWeek">The endweek.</param>
        /// <returns>
        /// 	<c>true</c> if the week is between the two dates; otherwise, <c>false</c>.
        /// </returns>
        public bool IsBetweenWeeks(Week startWeek, Week endWeek)
        {
            return value >= startWeek.value && value <= endWeek.value;
        }
        #endregion
		
        #region ISerializable Members
        private Week(SerializationInfo info, StreamingContext context)
        {
            if (info == null)
                throw new ArgumentNullException("info");

            value = info.GetInt32("value");
            begin = info.GetDateTime("begin");
            end = info.GetDateTime("end");
            year = info.GetInt32("year");
            number = info.GetInt32("number");
            calendar = info.GetValue("calendar", typeof(Calendar)) as Calendar;
            calendarWeekRule = (CalendarWeekRule)info.GetInt32("calendarWeekRule");
            firstDayOfWeek = (DayOfWeek)info.GetInt32("firstDayOfWeek");
        }

        [SecurityPermissionAttribute(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        {
            if (info == null)
                throw new ArgumentNullException("info");

            info.AddValue("value", value);
            info.AddValue("begin", begin);
            info.AddValue("end", end);
            info.AddValue("year", year);
            info.AddValue("number", number);
            info.AddValue("calendar", calendar, typeof(Calendar));
            info.AddValue("calendarWeekRule", (int)calendarWeekRule);
            info.AddValue("firstDayOfWeek", (int)firstDayOfWeek);
        }
        #endregion

        #region IComparable Members
        int IComparable.CompareTo(object obj)
        {
            if (obj is Week)
                return CompareTo((Week)obj);

            throw new ArgumentException(Resources.WeekTypeExpected, "obj");
        }
        #endregion

        #region IComparable<T> Members
        public int CompareTo(Week other)
        {
            return value.CompareTo(other.value);
        }
        #endregion

        #region IEquatable<T> members
        public bool Equals(Week other)
        {
            return value == other.value;
        }
        #endregion

        #region IFormattable Members
        public string ToString(string format, IFormatProvider formatProvider)
        {
            return this.ToString();
        }
        #endregion

        #region Override members
        public override bool Equals(object obj)
        {
            if (obj is Week)
                return Equals((Week)obj);

            return false;
        }

        public override int GetHashCode()
        {
            return value.GetHashCode();
        }

        public override string ToString()
        {
            return string.Format("W{0,-3} : {1:dd/MM/yyyy} - {2:dd/MM/yyyy}", Number, Begin, End);
        }
        #endregion

        #region Operators
        public static bool operator ==(Week x, Week y)
        {
            return x.Equals(y);
        }

        public static bool operator !=(Week x, Week y)
        {
            return !x.Equals(y);
        }

        public static Week operator +(Week x, int y)
        {
            return x.AddWeeks(y);
        }

        public static Week operator -(Week x, int y)
        {
            return x.AddWeeks(-y);
        }

        public static Week operator +(int x, Week y)
        {
            return y.AddWeeks(x);
        }

        public static Week operator -(int x, Week y)
        {
            return y.AddWeeks(-x);
        }

        public static int operator +(Week x, Week y)
        {
            throw new NotSupportedException();
        }

        public static int operator -(Week x, Week y)
        {
            var days = (x.Begin - y.Begin).Days;
            return days / 7;
        }

        public static bool operator <(Week x, Week y)
        {
            return x.value < y.value;
        }

        public static bool operator >(Week x, Week y)
        {
            return x.value > y.value;
        }

        public static bool operator <=(Week x, Week y)
        {
            return x.value <= y.value;
        }

        public static bool operator >=(Week x, Week y)
        {
            return x.value >= y.value;
        }
        #endregion
    }
}

